昨天把 Parser 的骨架搭好了,今天就直接來寫解析邏輯吧。
我先挑比較軟的柿子吃,從 RESP 裡三種單行資料型態開始:Simple Strings (簡單字串)、Errors (錯誤訊息) 與 Integers (整數)。它們長度不固定,但格式都很像,最後都用 \r\n (CRLF) 收尾。
+)Simple Strings 的格式為:
+<字串內容>\r\n
例如:
+OK\r\n
Day 3 已經先把 readLine() 寫出來,所以 Simple Strings 這邊就不用想太多:讀一行,轉成 Go 的字串,再包成 RESP Value。
// 在 code/resp/parser.go 中
func (r *Reader) readSimpleString() (Value, error) {
line, err := r.readLine()
if err != nil {
return Value{}, err
}
return NewSimpleString(string(line)), nil
}
在這裡,我將 byte slice 直接轉換成 string 並傳給 NewSimpleString 輔助函數。
-)Errors 的格式與 Simple Strings 幾乎一模一樣,唯一不同的是prefix byte為 -:
-<錯誤訊息>\r\n
例如:
-ERR unknown command\r\n
在 Redis 中,錯誤訊息也是一種一等公民(First-class citizen),客戶端收到以 - 開頭的訊息時,通常會丟出異常。我的解析方式同樣是讀取整行:
// 在 code/resp/parser.go 中
func (r *Reader) readError() (Value, error) {
line, err := r.readLine()
if err != nil {
return Value{}, err
}
return NewError(string(line)), nil
}
:)Integers 的格式為:
:<整數>\r\n
例如,表示數字 1000:
:1000\r\n
表示負數 50:
:-50\r\n
解析整數時,需要先使用 readLine() 讀取代表數字的byte array,接著將其解析為 Go 語言中的數值型別(在 Value 結構中,我使用 int 表示)。
解析整數我直接用 Go 標準庫的 strconv.ParseInt。Redis 的整數可能不小,所以先用 10 進位、64 位元去解析,再轉成目前 Value 裡使用的 int:
// 在 code/resp/parser.go 中
// readInteger 讀取行並解析為十進位整數,這個方法在解析 Bulk String 和 Array 的長度時也會被重複使用
func (r *Reader) readInteger() (int, error) {
line, err := r.readLine()
if err != nil {
return 0, err
}
// 將位元組 slice 轉為字串並解析
i64, err := strconv.ParseInt(string(line), 10, 64)
if err != nil {
return 0, fmt.Errorf("無效的整數格式: %w", err)
}
return int(i64), nil
}
// readIntegerType 是當prefix byte為 ':' 時的解析入口
func (r *Reader) readIntegerType() (Value, error) {
num, err := r.readInteger()
if err != nil {
return Value{}, err
}
return NewInteger(num), nil
}
這邊我在用 ParseInt 的時候曾因為 readLine 沒有濾乾淨 \r,導致 ParseInt 一直吐 error,卡了我好幾個小時才抓到兇手,有時候 bug 藏在細節裡真討厭。
透過這三個核心的子函數,ReadValue() 在收到對應的prefix byte後,就能輕鬆地分流解析:
讀取第一個 Byte(PrefixByte)
├── '+' ──▶ readSimpleString
├── '-' ──▶ readError
└── ':' ──▶ readIntegerType
這種靠字首分流的寫法很直覺,後面要補 Bulk String 或 Array 時,只要繼續往 switch 裡加分支,不用把整個 parser 重寫。
把解析器串上伺服器後,我們直接丟幾串 RESP 格式的字串給它解解看!
先啟動我們的伺服器:
$ go run ./code/main.go
打開另一個終端機,用 printf 搭配 nc 來送指令:
$ printf "+OK\r\n" | nc localhost 6379
# 預期回覆:(如果你在伺服器端有把解析完的 Value 印出來的話,會看到收到了 Simple String)
$ printf "-ERR something went wrong\r\n" | nc localhost 6379
# 預期回覆:(伺服器會解析出 Error 類型)
$ printf ":42\r\n" | nc localhost 6379
# 預期回覆:(伺服器會解析出 Integer 類型)
看到伺服器沒有 panic,而且能正確讀出字串跟整數,解析器第一階段階段性任務達成!
今天先把三種單行 RESP 型別解析完。看起來只是讀字串和轉數字,但 CRLF 沒處理好就會一路壞到後面的 parser。
明天要處理比較麻煩的 Bulk Strings 和 Arrays,一想到遞迴解析就有點頭痛,明天見啦!